/*
 * The MIT License
 *
 * Copyright 2012 http://generic-toolset.googlecode.com.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.rlpw.swing;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.lang.reflect.AccessibleObject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.EventObject;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import javax.swing.AbstractCellEditor;
import javax.swing.JTable;
import javax.swing.JTree;
import javax.swing.RowSorter.SortKey;
import javax.swing.SortOrder;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeExpansionListener;
import javax.swing.event.TreeWillExpandListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableModel;
import javax.swing.table.TableRowSorter;
import javax.swing.tree.ExpandVetoException;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreePath;
import org.rlpw.swing.JOTree.ReflectionTreeModel;
import org.rlpw.swing.event.ModelAdapter;
import org.rlpw.swing.event.ModelEvent;
import org.rlpw.swing.event.ModelReplacedEvent;
import org.rlpw.swing.table.Column;
import org.rlpw.swing.tree.Children;
import org.rlpw.swing.tree.Parent;

/**
 *
 * @param <T> the type of the elements
 */
public class JOTreeTable<T> extends JOTable<T> {

    private TreeTableCellRenderer tree;
    private TableColumn treeColumn;

    /**
     * Constructs a empty
     * <code>JOTreeTable</code>.
     *
     * @param c the type of displayed items which is used for the reflection
     */
    public JOTreeTable(Class<T> c) {
        this(c, new DefaultModel<T>(), null);
    }

    /**
     * Constructs a
     * <code>JOTreeTable</code>. The tree table is created using the specified
     * data model.
     *
     * @param c the type of displayed items which is used for the reflection
     * @param model the data model of displayed items
     */
    public JOTreeTable(Class<T> c, Model<T> model) {
        this(c, model, null);
    }

    /**
     * Constructs a empty
     * <code>JOTreeTable</code>.
     *
     * @param c the type of displayed items which is used for the reflection
     * @param r the {@link java.util.ResourceBundle} for column headlines.
     */
    public JOTreeTable(Class<T> c, ResourceBundle r) {
        this(c, new DefaultModel<T>(), r);
    }

    /**
     * Constructs a
     * <code>JOTreeTable</code>. The tree table is created using the specified
     * data model.
     *
     * @param c the type of displayed items which is used for the reflection
     * @param model the data model of displayed items
     * @param r the {@link java.util.ResourceBundle} for column headlines.
     */
    public JOTreeTable(Class<T> c, Model<T> model, ResourceBundle r) {
        super(c, model, r);
        tree = new TreeTableCellRenderer(c, model, "Root");

        final TableModel tableModel = getModel();

        setModel(new AbstractTableModel() {

            @Override
            public int getRowCount() {
                return getTree().getRowCount();
            }

            @Override
            public int getColumnCount() {
                return tableModel.getColumnCount();
            }

            @Override
            public String getColumnName(int columnIndex) {
                return tableModel.getColumnName(columnIndex);
            }

            @Override
            public Class<?> getColumnClass(int columnIndex) {
                return tableModel.getColumnClass(columnIndex);
            }

            @Override
            public boolean isCellEditable(int rowIndex, int columnIndex) {
                if (getTree().isRootVisible() && rowIndex == 0) {
                    return false;
                }
                return tableModel.isCellEditable(convertRow(rowIndex), columnIndex);
            }

            @Override
            public Object getValueAt(int rowIndex, int columnIndex) {
                if (getTree().isRootVisible() && rowIndex == 0) {
                    return null;
                }

                return tableModel.getValueAt(convertRow(rowIndex), columnIndex);
            }

            @Override
            public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
                if (!(getTree().isRootVisible() && rowIndex == 0)) {
                    tableModel.setValueAt(aValue, convertRow(rowIndex), columnIndex);
                }

            }

            private int convertRow(int row) {
                List<T> expandedItems = tree.getExpandedItems();
                return JOTreeTable.this.getVisibleItems().indexOf(expandedItems.get(row));
            }
        });

        setIntercellSpacing(new Dimension(0, 0));
        setShowGrid(false);

        treeColumn = getTreeColumn(c);
        treeColumn.setCellRenderer((TableCellRenderer) tree);
        treeColumn.setCellEditor(new TreeTableCellEditor());

        setRowSorter(new TreeTableRowSorter());
    }

    /**
     * Returns the internal {@link org.rlpw.swing.JOTree} component.
     *
     * @return the internal tree component
     */
    public JOTree<T> getTree() {
        return tree;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setFilter(Filter<T> filter) {
        if (getTree() != null) {
            tree.setFilter0(filter);
        }
        super.setFilter(filter);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setSorter(Comparator<T> comp) {
        if (getTree() != null) {
            tree.setSorter0(comp);
        }
        super.setSorter(comp);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setDataModel(Model<T> model) {
        if (getTree() != null) {
            getTree().setDataModel(model);
        }
        super.setDataModel(model);

    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void updateView() {
        try {
            ((AbstractTableModel) getModel()).fireTableDataChanged();
        } catch (Exception e) {
        }
        super.updateView();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public SelectModel<T> getSelectModel() {
        return new SelectModelImpl();
    }

    private TableColumn getTreeColumn(Class<T> c) {
        for (ReflectionData rd : getReflectionDataList()) {
            if (isTreeColumn(rd.getAccessibleObject())) {
                return getColumn(rd.getName());
            }
        }
        return null;
    }

    private boolean isTreeColumn(AccessibleObject ao) {
        return ao.isAnnotationPresent(Column.class) && (ao.isAnnotationPresent(Parent.class) || ao.isAnnotationPresent(Children.class));
    }

    private class SelectModelImpl implements SelectModel<T> {

        @Override
        public boolean isSelected(int index) {
            rangeCheck(index);
            if (getRowSorter() != null) {
                return getSelectionModel().isSelectedIndex(getRowSorter().convertRowIndexToView(index));
            }
            return getSelectionModel().isSelectedIndex(index);
        }

        @Override
        public boolean isSelected(T element) {
            return isSelected(tree.getExpandedItems().indexOf(element));
        }

        @Override
        public void setSelected(int index, boolean select) {
            rangeCheck(index);
            if (getRowSorter() != null) {
                index = getRowSorter().convertRowIndexToView(index);
            }
            if (select) {
                getSelectionModel().addSelectionInterval(index, index);
            } else {
                getSelectionModel().removeSelectionInterval(index, index);
            }
        }

        @Override
        public void setSelected(T element, boolean select) {
            setSelected(tree.getExpandedItems().indexOf(element), select);
        }

        @Override
        public void setAllSelected(boolean select) {
            if (getSelectionModel().getSelectionMode() == SelectionMode.SINGLE) {
                return;
            } else if (select) {
                getSelectionModel().setSelectionInterval(0, tree.getExpandedItems().size() - 1);
            } else {
                getSelectionModel().clearSelection();
            }
        }

        @Override
        public List<T> getSelected() {
            List<T> result = new ArrayList<>();
            for (Object obj : tree.getExpandedItems()) {
                try {
                    if (isSelected((T) obj)) {
                        result.add((T) obj);
                    }
                } catch (Exception e) {
                }
            }
            return result;
        }

        @Override
        public int countSelected() {
            return getSelected().size();
        }

        @Override
        public void setMode(int mode) {
            getSelectionModel().setSelectionMode(mode);
        }

        @Override
        public int getMode() {
            return getSelectionModel().getSelectionMode();
        }

        private void rangeCheck(int index) {
            int size = tree.getExpandedItems().size();
            if (index >= size) {
                throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
            }
        }
    }

    private class TreeTableCellRenderer extends JOTree<T> implements TableCellRenderer {

        private int visibleRow;
        private List expandedItems;
        private List<T> selectedItems;
        private SelectModel<T> selectModel;

        public TreeTableCellRenderer(Class<T> c, Model<T> model, Object root) {
            super(c, model, root);
            setRowHeight(JOTreeTable.this.getRowHeight());
            selectModel = getSelectModel();

            addTreeWillExpandListener(new TreeWillExpandListener() {

                @Override
                public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException {
                    try {
                        selectedItems = selectModel.getSelected();
                    } catch (Exception e) {
                    }
                }

                @Override
                public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException {
                    try {
                        selectedItems = selectModel.getSelected();
                    } catch (Exception e) {
                    }
                }
            });
            addTreeExpansionListener(new TreeExpansionListener() {

                @Override
                public void treeExpanded(TreeExpansionEvent event) {
                    expandedItems = null;
                    JOTreeTable.this.getRowSorter().modelStructureChanged();
                    select();
                }

                @Override
                public void treeCollapsed(TreeExpansionEvent event) {
                    expandedItems = null;
                    JOTreeTable.this.getRowSorter().modelStructureChanged();
                    select();
                }
            });

            model.addModelListener(new ModelAdapter<T>() {

                @Override
                public void itemAdded(ModelEvent<T> e) {
                    expandedItems = null;
                }

                @Override
                public void itemRemoved(ModelEvent<T> e) {
                    expandedItems = null;
                }

                @Override
                public void itemReplaced(ModelReplacedEvent<T> e) {
                    expandedItems = null;
                }

                @Override
                public void allItemsRemoved(EventObject e) {
                    expandedItems = null;
                }
            });

            putClientProperty("JTree.lineStyle", "None");
            super.setRootVisible(false);
            expandAll();
        }

        private void expandAll() {
            for (int i = 0; i < getRowCount(); i++) {
                expandRow(i);
            }
        }

        private void select() {
            if (selectedItems == null) {
                return;
            }
            selectModel.setAllSelected(false);
            for (T item : selectedItems) {
                selectModel.setSelected(item, true);
            }
        }

        public List getExpandedItems() {
            if (expandedItems == null) {
                List ls = new ArrayList<>();
                for (int j = 0; j < getRowCount(); j++) {
                    ls.add(getPathForRow(j).getLastPathComponent());
                }
                expandedItems = ls;
            }
            return expandedItems;
        }

        @Override
        public void setBounds(int x, int y, int w, int h) {
            super.setBounds(x, 0, w, JOTreeTable.this.getHeight());
        }

        @Override
        public void paint(Graphics g) {
            Rectangle cellRect = JOTreeTable.this.getCellRect(visibleRow, 0, false);
            g.translate(0, -cellRect.y);

            super.paint(g);
        }

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
            if (isSelected) {
                setBackground(table.getSelectionBackground());
            } else {
                setBackground(table.getBackground());
            }

            setVisibleRow(JOTreeTable.this.getRowSorter().convertRowIndexToModel(row));
            return this;
        }

        public void setVisibleRow(int visibleRow) {
            this.visibleRow = getTreeRow(visibleRow);
        }

        @Override
        public SelectModel<T> getSelectModel() {
            return JOTreeTable.this.getSelectModel();
        }

        @Override
        public void setDragAndDropSupport(boolean enabled) {
            super.setDragAndDropSupport(enabled);
            JOTreeTable.this.setDragEnabled(enabled);
            JOTreeTable.this.setDropTarget(enabled ? getTree().getDropTarget() : null);

        }

        @Override
        public void setRootVisible(boolean rootVisible) {
            super.setRootVisible(rootVisible);
            expandedItems = null;
            getRowSorter().allRowsChanged();
        }

        @Override
        public void setFilter(Filter<T> filter) {
            JOTreeTable.this.setFilter(filter);
        }

        @Override
        public void setSorter(Comparator<T> comp) {
            JOTreeTable.this.setSorter(comp);
        }

        void setSorter0(Comparator<T> comp) {
            super.setSorter(comp);
        }

        void setFilter0(Filter<T> filter) {
            super.setFilter(filter);
        }

        @Override
        public void setCellRenderer(final TreeCellRenderer renderer) {
            if (renderer == null) {
                super.setCellRenderer(renderer);
            } else {
                super.setCellRenderer(new TreeCellRenderer() {

                    @Override
                    public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
                        try {
                            T item = (T) value;
                            selected = getSelectModel().isSelected(item);
                        } catch (Exception e) {
                        }
                        return renderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
                    }
                });
            }
        }

        @Override
        public TreePath getPathForLocation(int x, int y) {
            return super.getPathForLocation(convertX(x), convertY(y));
        }

        int convertX(int x) {
            return x - getCellRect(0, JOTreeTable.this.getColumnModel().getColumnIndex(treeColumn.getIdentifier()), true).x;
        }

        int convertY(int y) {
            int tableRow = y / getRowHeight();
            int treeRow = getTreeRow(JOTreeTable.this.getRowSorter().convertRowIndexToModel(tableRow));
            return treeRow * getRowHeight() + (y - tableRow * getRowHeight());
        }

        private int getTreeRow(int modelRow) {
            Object o = tree.getExpandedItems().get(modelRow);
            for (int i = 0; i < getRowCount(); i++) {
                if (getTree().getPathForRow(i).getLastPathComponent().equals(o)) {
                    return i;
                }
            }
            return -1;
        }
    }

    private class TreeTableCellEditor extends AbstractCellEditor implements TableCellEditor {

        private Object value;

        @Override
        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int r, int c) {
            this.value = value;
            return getTree();
        }

        @Override
        public Object getCellEditorValue() {
            return value;
        }

        @Override
        public boolean isCellEditable(EventObject e) {
            if (e instanceof MouseEvent) {
                MouseEvent me = (MouseEvent) e;
                MouseEvent newME = new MouseEvent(getTree(), me.getID(),
                        me.getWhen(), me.getModifiers(),
                        tree.convertX(me.getX()), tree.convertY(me.getY()), me.getClickCount(),
                        me.isPopupTrigger());
                try {
                    tree.dispatchEvent(newME);
                } catch (Exception ex) {
                }
            }
            return false;
        }
    }

    private class TreeTableRowSorter extends TableRowSorter<TableModel> {

        private List<? extends SortKey> keys = new ArrayList<>();
        private Converter<Integer, Integer> converter = new Converter<>();

        public TreeTableRowSorter() {
            setModel(JOTreeTable.this.getModel());
            sort();
        }

        @Override
        public int convertRowIndexToModel(int index) {
            return converter.left(index);
        }

        @Override
        public int convertRowIndexToView(int index) {
            return converter.right(index);
        }

        @Override
        public void setSortKeys(List<? extends SortKey> keys) {
            List<SortKey> old = (List<SortKey>) getSortKeys();
            for (SortKey key : keys) {
                if (key.getColumn() < 0 || key.getColumn() >= getModel().getColumnCount()) {
                    throw new IllegalArgumentException("Invalid SortKey");
                }

            }
            this.keys = Collections.unmodifiableList(new ArrayList<>(keys));
            if (!this.keys.equals(old)) {
                fireSortOrderChanged();
            }
            sort();
        }

        @Override
        public List<? extends SortKey> getSortKeys() {
            return keys;
        }

        @Override
        public int getViewRowCount() {
            return getModelRowCount();
        }

        @Override
        public int getModelRowCount() {
            return getModel().getRowCount();
        }

        @Override
        public void modelStructureChanged() {
            sort();
        }

        @Override
        public void allRowsChanged() {
            sort();
        }

        @Override
        public void rowsInserted(int firstRow, int endRow) {
            sort();
        }

        @Override
        public void rowsDeleted(int firstRow, int endRow) {
            sort();
        }

        @Override
        public void rowsUpdated(int firstRow, int endRow) {
            sort();
        }

        @Override
        public void rowsUpdated(int firstRow, int endRow, int column) {
            sort();
        }

        @Override
        public void sort() {
            int[] last = new int[converter.size()];
            for (int i = 0; i < last.length; i++) {
                last[i] = converter.left(i);
            }
            converter.clear();
            List model = tree.getExpandedItems();
            List visibleItems = new ArrayList<>(model);
            Collections.sort(visibleItems, new Comparator() {

                Comparator comparator = getComparator();
                ReflectionTreeModel rtm = (ReflectionTreeModel) getTree().getModel();

                Object getAncestor(Object o1, Object o2) {
                    if (o1 != null && o2 != null && rtm.getParent(o1) != null && rtm.getParent(o1).equals(rtm.getParent(o2))) {
                        return rtm.getParent(o1);
                    }

                    if (o1 != null && o1.equals(o2)) {
                        return o1;
                    }

                    Object ancestor = null;
                    if (o1 != null) {
                        ancestor = getAncestor(rtm.getParent(o1), o2);
                    }
                    if (ancestor == null && o2 != null) {
                        ancestor = getAncestor(o1, rtm.getParent(o2));
                    }
                    return ancestor;

                }

                Object getComparableObject(Object o, Object ancestor) {
                    if (o.equals(ancestor)
                            || (rtm.getParent(o) == null && ancestor == null)
                            || rtm.getParent(o).equals(ancestor)) {
                        return o;
                    } else {
                        return getComparableObject(rtm.getParent(o), ancestor);
                    }

                }

                @Override
                public int compare(Object o1, Object o2) {

                    if (getTree().getRoot().equals(o1)) {
                        return -1;
                    } else if (getTree().getRoot().equals(o2)) {
                        return 1;
                    }

                    if ((rtm.getParent(o1) == null && rtm.getParent(o2) == null)
                            || (rtm.getParent(o1) != null && rtm.getParent(o1).equals(rtm.getParent(o2)))) {
                        return comparator.compare(o1, o2);

                    } else {

                        Object parent = o2;
                        while (!parent.equals(getTree().getRoot())) {
                            if (o1.equals(parent)) {
                                return -1;
                            }
                            parent = rtm.getParent(parent);
                            if (parent == null) {
                                parent = getTree().getRoot();
                            }
                        }
                        parent = o1;
                        while (!parent.equals(getTree().getRoot())) {
                            if (o2.equals(parent)) {
                                return 1;
                            }
                            parent = rtm.getParent(parent);
                            if (parent == null) {
                                parent = getTree().getRoot();
                            }
                        }

                        Object ancestor = getAncestor(o1, o2);
                        return comparator.compare(getComparableObject(o1, ancestor), getComparableObject(o2, ancestor));
                    }
                }
            });

            for (int i = 0; i < getViewRowCount(); i++) {
                int index = visibleItems.indexOf(model.get(i));
                converter.put(i, index == -1 ? 0 : index);
            }
            fireRowSorterChanged(last);
        }

        Comparator getComparator() {
            return new Comparator() {

                @Override
                public int compare(Object o1, Object o2) {
                    if (getSortKeys().isEmpty()) {
                        return compare(new SortKey(0, SortOrder.UNSORTED), o1, o2);
                    }
                    int compare = 0;
                    for (int i = 0; i < getSortKeys().size() && compare == 0; i++) {
                        compare = compare(getSortKeys().get(i), o1, o2);
                    }
                    return compare;
                }

                private int compare(SortKey key, Object o1, Object o2) {
                    List<ReflectionData> refs = JOTreeTable.this.getReflectionDataList();
                    int compare = 0;
                    try {
                        compare = getComparator(key.getColumn()).compare(
                                key.getColumn() == 0 ? o1 : refs.get(key.getColumn()).get(o1),
                                key.getColumn() == 0 ? o2 : refs.get(key.getColumn()).get(o2));
                    } catch (Exception ex) {
                        if (key.getColumn() == 0) {
                            if (JOTreeTable.this.getSorter() != null) {
                                compare = JOTreeTable.this.getSorter().compare((T) o1, (T) o2);
                            } else {
                                compare = ((Integer) tree.getExpandedItems().indexOf(o1)).compareTo((Integer) tree.getExpandedItems().indexOf(o2));
                            }
                        }
                    }
                    if (compare != 0 && key.getSortOrder().equals(SortOrder.DESCENDING)) {
                        compare *= -1;
                    }

                    return compare;
                }
            };
        }

        @Override
        public Comparator getComparator(int column) {
            return super.getComparator(column);
        }

        private class Converter<L, R> {

            private Map<L, R> leftToRight = new HashMap<>();
            private Map<R, L> rightToLeft = new HashMap<>();

            public void put(L left, R right) {
                leftToRight.put(left, right);
                rightToLeft.put(right, left);
            }

            public L left(R right) {
                return rightToLeft.get(right);
            }

            public R right(L left) {
                return leftToRight.get(left);
            }

            public void clear() {
                leftToRight.clear();
                rightToLeft.clear();
            }

            public int size() {
                return leftToRight.size();
            }
        }
    }
}
